﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using SimpleRest.Core.Routing;
using System.Web.Routing;
using System.Collections.ObjectModel;
using System.Web;
using SimpleRest.Core.Exceptions;

namespace SimpleRest.Core.Routing
{
    /// <summary>
    /// Delegate used to drive event handling for service route eventing.
    /// </summary>
    /// <param name="route"></param>
    public delegate void ServiceRouteRegisteredHandler(ServiceRoute route);
    /// <summary>
    /// Delegate used to drive operation route event handling.
    /// </summary>
    /// <param name="route"></param>
    public delegate void OperationRouteRegisteredHandler(OperationRoute operation, Route route);
    public delegate void IncomingRequestEventHandler(IncomingHttpRequestEventArgs e);
    public delegate void HttpRequestBodyResolvedEventHandler(Type service, Operation operation, object body);

    /// <summary>
    /// Route utility to assist with core routing functionality and provides help resolving and finding registered routes.
    /// </summary>
    public static class RouteUtility
    {
        /// <summary>
        /// Defaults to FALSE.
        /// When set to true the System.Web.Routing.RouteTable will be used to route requests.
        /// When set to false a custom route table is used which will not interfere with ASP.NET MVC routes (or any other routes).
        /// </summary>
        internal static bool IsNativeRouteRegistrationEnabled { get; set; }
        private static Collection<RouteBase> _routes = new Collection<RouteBase>();
        public static Collection<RouteBase> Routes
        {
            get
            {
                if (IsNativeRouteRegistrationEnabled)
                    return System.Web.Routing.RouteTable.Routes;
                else
                    return _routes;
            }
            internal set
            {
                _routes = value;
            }
        }

        /// <summary>
        /// Get route data associated to relative route uri and http method.
        /// </summary>
        /// <param name="relativeRouteUri">Example: /resource/123/SubResources</param>
        /// <param name="httpMethod">Example: GET</param>
        /// <returns></returns>
        public static RouteData GetRouteData(string relativeRouteUri, string httpMethod)
        {
            if (!relativeRouteUri.StartsWith("/"))
                relativeRouteUri = "/" + relativeRouteUri;

            if (string.IsNullOrEmpty(httpMethod))
                throw new ArgumentNullException("httpMethod");
            if (string.IsNullOrEmpty(relativeRouteUri))
                throw new ArgumentNullException("requestUri");
            if (!Uri.IsWellFormedUriString(relativeRouteUri, UriKind.Relative))
                throw new ArgumentException("request uri is not a well formed relative uri.");

            // first token in SimpleRest must be a non variable token.
            var path = relativeRouteUri.Trim(new char[] { '/' });
            string service = path.Split(new char[] { '/' })[0];

            var pathSplit = path.Split(new char[] { '/' });
            IEnumerable<OperationRoute> items = RegisteredOperations.Where(
                d => d.RouteUri.StartsWith(service) &&
                    d.HttpMethod.ToUpper() == httpMethod.ToUpper() &&
                    d.RouteUri.Trim(new char[] { '/' }).Split(new char[] { '/' }).Length == pathSplit.Length);

            if (items == null || !items.Any())
                return null;

            List<OperationRoute> itemLists = items.ToList();
            OperationRoute targetRoute = null;
            List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();

            foreach (var operation in itemLists)
            {
                var operationRouteUriSplit = operation.RouteUri.Trim(new char[] { '/' }).Split(new char[] { '/' });
                bool isTargetOperation = true;

                for (int i = 1; i < operationRouteUriSplit.Length; i++)
                {
                    // do not allow parameters in operation uri that do not start with { and end with }
                    if (operationRouteUriSplit[i].StartsWith("{") && operationRouteUriSplit[i].EndsWith("}"))
                    {
                        values.Add(new KeyValuePair<string,object>(operationRouteUriSplit[i].Substring(1, operationRouteUriSplit[i].Length-2), pathSplit[i]));
                    }
                    else if (!operationRouteUriSplit[i].ToLower().Equals(pathSplit[i].ToLower()))
                    {
                        values.Clear();
                        isTargetOperation = false;
                        break;
                    }
                }

                if (isTargetOperation)
                {
                    targetRoute = operation;
                    break;
                }
            }

            if (targetRoute == null && itemLists.Count > 1)
                throw new Exception("More than one route is registered with the associated request.");

            RouteData routeData = new RouteData(targetRoute._route, targetRoute._route.RouteHandler);

            foreach (KeyValuePair<string, object> value in values)
                routeData.Values.Add(value.Key, value.Value);
          
            return routeData;
        }

        /// <summary>
        /// Event raised when a service route gets registered.
        /// </summary>
        public static event ServiceRouteRegisteredHandler ServiceRouteRegistered;
        /// <summary>
        /// Event raised when a service operation gets registered.
        /// </summary>
        public static event OperationRouteRegisteredHandler OperationRouteRegistered = delegate { };
        public static event IncomingRequestEventHandler IncomingRequest = delegate { };
        public static event HttpRequestBodyResolvedEventHandler HttpRequestBodyResolved = delegate { };
        private static object _serviceLock = new object();
        private static object _operationLock = new object();

        private static Dictionary<ServiceRoute, List<ServiceRoute>> RegisteredServices = new Dictionary<ServiceRoute, List<ServiceRoute>>();
        private static List<OperationRoute> RegisteredOperations = new List<OperationRoute>();

        /// <summary>
        /// Statically cached list for method associations to operations
        /// </summary>
        private static Dictionary<MethodInfo, Operation> MethodOperations { get; set; }
        /// <summary>
        /// Statically cached list for services to operations.
        /// </summary>
        private static Dictionary<Type, List<Operation>> ServiceOperations { get; set; }
        /// <summary>
        /// Statically cached lists of services by type that are tagged as IsReusable=false.
        /// </summary>
        private static Dictionary<Type, object> ServiceInstances { get; set; }

        /// <summary>
        /// Determines if a route uri is currently registered.
        /// </summary>
        /// <param name="routeUri"></param>
        /// <returns></returns>
        public static bool IsServiceRouteRegistered(Route route)
        {
            string routeUri = route.Url.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' });
            var serviceItems = GetRegisteredServices().Where(d => d.Route.Url.Split(new char[] { '?'})[0].Trim(new char[] { '/', '\\'}).ToLower() == routeUri);
            if (serviceItems != null && serviceItems.Count() > 0)
                return true;

            return false;
        }
        public static bool IsServiceRouteRegistered(string routeUri)
        {
            routeUri = routeUri.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' });
            var serviceItems = GetRegisteredServices().Where(d => d.Route.Url.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' }).ToLower() == routeUri);
            if (serviceItems != null && serviceItems.Count() > 0)
                return true;

            return false;
        }

        public static bool IsOperationRouteRegistered(Route route, string targetHttpMethod)
        {
            string routeUri = route.Url.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' });
            var operationItems = RegisteredOperations.Where(d => d.Route.Url.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' }).ToLower() == routeUri && d.HttpMethod.ToLower().Equals(targetHttpMethod.ToLower()));
            if (operationItems != null && operationItems.Count() > 0)
                return true;

            return false;
        }
        public static bool IsOperationRouteRegistered(string routeUri, string targetHttpMethod)
        {
            routeUri = routeUri.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' });
            var operationItems = RegisteredOperations.Where(d => d.Route.Url.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\' }).ToLower() == routeUri && d.HttpMethod.ToLower().Equals(targetHttpMethod.ToLower()));
            if (operationItems != null && operationItems.Count() > 0)
                return true;

            return false;
        }

        /// <summary>
        /// Returns a list of registered service routes.
        /// </summary>
        /// <returns></returns>
        public static List<ServiceRoute> GetRegisteredServices()
        {
            return RegisteredServices.Select(d => d.Key).ToList();
        }
        /// <summary>
        /// Returns a list of registered operation routes.
        /// </summary>
        /// <returns></returns>
        public static List<OperationRoute> GetRegisteredOperations()
        {
            return RegisteredOperations;
        }

        static RouteUtility()
        {
            ServiceRouteRegistered += new ServiceRouteRegisteredHandler(RouteUtil_ServiceRouteRegistered);
            OperationRouteRegistered += new OperationRouteRegisteredHandler(RouteUtil_OperationRouteRegistered);
            MethodOperations = new Dictionary<MethodInfo, Operation>();
            ServiceOperations = new Dictionary<Type, List<Operation>>();
            ServiceInstances = new Dictionary<Type, object>();
            IsNativeRouteRegistrationEnabled = true;
        }


        static void RouteUtil_OperationRouteRegistered(OperationRoute operation, Route route)
        {
            lock (_operationLock)
            {
                RegisteredOperations.Add(operation);
            }
        }
        static void RouteUtil_ServiceRouteRegistered(ServiceRoute route)
        {
            lock (_serviceLock)
            {
                RegisteredServices.Add(route, new List<ServiceRoute>(){ route });
            }
        }

        internal static void raiseServiceRouteRegistration(ServiceRoute route)
        {
            ServiceRouteRegistered(route);
        }
        internal static void raiseOperationRouteRegistration(OperationRoute operation, Route route)
        {
            OperationRouteRegistered(operation, route);
        }
        internal static void raiseIncomingRequest(RequestContext context, IHttpRequestMessage request)
        {
            IncomingRequest(new IncomingHttpRequestEventArgs(context, request));
        }
        internal static void raiseHttpRequestBodyResolved(Type service, Operation operation, object obj)
        {
            HttpRequestBodyResolved(service, operation, obj);
        }

        /// <summary>
        /// Return correct route prefix for service type specified.
        /// </summary>
        /// <param name="type">Service type</param>
        /// <returns></returns>
        public static string GetRoutePrefix(Type type)
        {
            if (type == null)
                throw new Exception("Unable to obtain route prefix for null type.");

            foreach (var attribute in type.GetCustomAttributes(true))
            {
                if (attribute is ServiceAttribute)
                {
                    var attr = attribute as ServiceAttribute;
                    return attr.GetVersionServiceRoot().TrimEnd(new char[] { '/', '\\' });
                }
            }

            throw new Exception("Route prefix could not be determined.");
        }
        /// <summary>
        /// Return correct route prefix for service type specified suffixed with uri template specified.
        /// </summary>
        /// <param name="type">Service type.</param>
        /// <param name="uriTemplate">Route uri to suffix result with.</param>  
        /// <returns></returns>
        public static string GetRoutePrefix(Type type, string uriTemplate)
        {
            if (uriTemplate == null) uriTemplate = "";
            var uriSplit = uriTemplate.Split(new char[] { '?' });
            if (uriSplit.Length > 0)
                uriTemplate = uriSplit[0];

            return GetRoutePrefix(type) + "/" + uriTemplate.Trim(new char[] { '/', '\\' });
        }
        /// <summary>
        /// Return correct route prefix for service type specified suffixed with uri template associated to method.
        /// </summary>
        /// <param name="type">Service type.</param>
        /// <param name="method">Method to evaluate and find UriTemplate.</param>
        /// <returns></returns>
        public static string GetRoutePrefix(Type type, System.Reflection.MethodInfo method)
        {
            foreach (var attr in method.GetCustomAttributes(true))
                if (attr is IWebInvokeAttribute)
                {
                    return GetRoutePrefix(type, (attr as IWebInvokeAttribute).UriTemplate);
                }

            throw new Exception("Route prefix could not be determined.");
        }
        public static string GetRouteUri(System.Reflection.MethodInfo method)
        {
            foreach (var attr in method.GetCustomAttributes(true))
                if (attr is IWebInvokeAttribute)
                {
                    var routeUri = (attr as IWebInvokeAttribute).UriTemplate;
                    return routeUri == null ? "" : routeUri;
                }

            throw new Exception("Route Uri could not be determined.");
        }

        /// <summary>
        /// Return the correct Http Method for the associated attribute for the associated method info.
        /// </summary>
        /// <param name="type">operation MethodInfo object</param>
        /// <returns></returns>
        public static string GetHttpMethod(MethodInfo type)
        {
            foreach (var attr in type.GetCustomAttributes(true))
                if (attr is IWebInvokeAttribute)
                {
                    return (attr as IWebInvokeAttribute).Method;
                    break;
                }
            return "";
        }

        /// <summary>
        /// Return the associated Operation from the method info provided
        /// </summary>
        /// <param name="method">Operation method info class.</param>
        /// <returns></returns>
        internal static Operation GetOperation(string routeUriBase, MethodInfo method)
        {
            if (method == null)
                return null;

            var attributes = method.GetCustomAttributes(false);
            IWebInvokeAttribute webInvokeAttribute = null;

            foreach (var attr in attributes)
                if (attr is IWebInvokeAttribute)
                {
                    webInvokeAttribute = attr as IWebInvokeAttribute;
                    break;
                }

            if (webInvokeAttribute == null)
                return null;

            if (!MethodOperations.ContainsKey(method))
            {
                List<OperationParameter> methodParameters = new List<OperationParameter>();
                foreach (var methodParameter in method.GetParameters())
                {
                    bool isOptional = false;
                    var split = webInvokeAttribute.UriTemplate.Split(new char[] { '?' });
                    string root = "";
                    string query = "";

                    if (split.Length > 0)
                        root = split[0];
                    if (split.Length > 1)
                        query = split[1];

                    if (root.ToLower().Contains("{" + methodParameter.Name.ToLower() + "}"))
                        isOptional = false;
                    else if (query.ToLower().Contains("{" + methodParameter.Name.ToLower() + "}"))
                        isOptional = true;
                    else isOptional = true; // TODO: determine if this is the right course of action.

                    bool isHttpBody = false;
                    foreach (object attr in methodParameter.GetCustomAttributes(true))
                    {
                        if (attr is HttpBodyAttribute)
                        {
                            isHttpBody = true;
                            break;
                        }
                    }

                    OperationParameter parameter = new OperationParameter(methodParameter.ParameterType, methodParameter.Name, isOptional, isHttpBody);

                    methodParameters.Add(parameter);
                }

                Operation operation = new Operation(method.Name, method.ReturnType, routeUriBase + "/" + webInvokeAttribute.UriTemplate, method, webInvokeAttribute.Method, methodParameters);
                MethodOperations[method] = operation;
            }

            return MethodOperations[method];
        }
        /// <summary>
        /// Return a list of operations for the associated service type.
        /// </summary>
        /// <param name="service">Service type.</param>
        /// <returns></returns>
        public static List<Operation> GetOperations(Type service)
        {
            if (!ServiceOperations.ContainsKey(service))
            {
                List<Operation> operations = new List<Operation>();

                foreach (var method in service.GetMethods())
                {
                    foreach (var attr in method.GetCustomAttributes(true))
                    {
                        if (attr is IWebInvokeAttribute)
                        {
                            operations.Add(GetOperation(GetRoutePrefix(service), method));
                            break;
                        }
                    }
                }
                ServiceOperations.Add(service, operations);
            }

            return ServiceOperations[service];
        }

        /// <summary>
        /// Create an instance of a service and add it to cache if appropriate.
        /// </summary>
        /// <param name="service"></param>
        /// <param name="createInstanceHandler"></param>
        /// <returns></returns>
        internal static object CreateInstance(Type service, IHttpRequestMessage request, CreateInstanceHandler createInstanceHandler)
        {
            if (!ServiceInstances.ContainsKey(service))
            {
                object obj = createInstanceHandler.Invoke(service, request);
                if (service.IsServiceExtension())
                {
                    if ((obj as ServiceBase)._isReusable)
                    {
                        ServiceInstances.Add(service, obj);
                    }
                }
                else
                    ServiceInstances.Add(service, obj);

                return obj;
            }

            return ServiceInstances[service];
        }
        /// <summary>
        /// Extension method that determines if the service type specified extends ServiceExtension.
        /// </summary>
        /// <param name="service">Service type.</param>
        /// <returns></returns>
        internal static bool IsServiceExtension(this Type service)
        {
            return service.IsSubclassOf(typeof(ServiceBase)) || typeof(ServiceBase).IsSubclassOf(service);
        }

        /// <summary>
        /// Return the MethodInfo object for the associated service type, route, and request uri.
        /// </summary>
        /// <param name="service">Service type.</param>
        /// <param name="route">Route to be examined.</param>
        /// <param name="uri">Incoming Uri to be overlayed against route to determine method.</param>
        /// <returns></returns>
        public static MethodInfo GetOperationMethod(Type service, string route, string uri, string requestHttpMethod)
        {
            if (service == null)
                throw new Exception("Unable to determine operation from null service type.");
            if (string.IsNullOrEmpty(route))
                throw new Exception("Unable to determine operation from empty route value.");
            if (string.IsNullOrEmpty(uri))
                throw new Exception("Unable to determine operation from empty uri value.");

            ServiceAttribute serviceAttribute = null;

            foreach (var attr in service.GetCustomAttributes(true))
                if (attr is ServiceAttribute)
                {
                    serviceAttribute = attr as ServiceAttribute;
                    break;
                }

            if (serviceAttribute == null)
                throw new Exception("Unable to determine operation from type which is not indicated as a valid service.");

            string scrubbedRoute = route.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\', ' ' }).Replace("\\", "/");
            string scrubbedServiceRoute = serviceAttribute.GetVersionServiceRoot().Trim(new char[] { '/', '\\', ' ' }).Replace("\\", "/");
            foreach (var method in service.GetMethods())
            {
                IWebInvokeAttribute webInvokeAttribute = null;
                foreach (var attr in method.GetCustomAttributes(true))
                    if (attr is IWebInvokeAttribute)
                    {
                        webInvokeAttribute = attr as IWebInvokeAttribute;
                        break;
                    }

                if (webInvokeAttribute != null)
                {
                    string scrubbedWebInvokeRoute =
                        webInvokeAttribute.UriTemplate == null ? "" : webInvokeAttribute.UriTemplate.Split(new char[] { '?' })[0].Trim(new char[] { '/', '\\', ' ' }).Replace("\\", "/");

                    if (webInvokeAttribute.Method.ToLower() == requestHttpMethod.ToLower() && scrubbedRoute.ToLower().Equals((string.IsNullOrEmpty(scrubbedWebInvokeRoute) ? scrubbedServiceRoute : scrubbedServiceRoute + "/") + scrubbedWebInvokeRoute.ToLower()))
                        return method;
                }
            }

            return null; // could not find an associated method.
        }

        /// <summary>
        /// Register a REST service route against the specified routing collection.
        /// </summary>
        /// <param name="collection">Route collection object in which the service route will be added.</param>
        /// <param name="route">Service route to be registered.</param>
        public static void Add(this Collection<RouteBase> collection, ServiceRoute route)
        {
            AddRoute(collection, route);
        }
        public static void Add(this Collection<RouteBase> collection, OperationRoute route, string targetHttpMethod)
        {
            AddRoute(collection, route, targetHttpMethod);
        }
        public static List<ServiceRoute> GetRegisteredServiceRoutes(string routeUri)
        {
            if (RegisteredServices == null) return null;

            List<ServiceRoute> routes = new List<ServiceRoute>();

            foreach (var svc in RegisteredServices.Where(d => d.Key.RouteUri.TrimRoute() == routeUri.TrimRoute()))
                foreach (var item in svc.Value)
                    routes.Add(item);
            return routes;
        }
        public static List<ServiceRoute> GetRegisteredServiceRoutes(Type service)
        {
            if (!service.IsValidService())
                throw new Exception("Type specified is not a valid service.");

            return GetRegisteredServiceRoutes(GetRoutePrefix(service));
        }

        public static void AddRoute(Collection<RouteBase> collection, OperationRoute route, string targetHttpMethod)
        {
            if (string.IsNullOrEmpty(targetHttpMethod)) throw new ArgumentNullException("TargetHttpMethod can not be empty or null when registering an operation route.");

            if (RouteUtility.IsOperationRouteRegistered(route.Route, targetHttpMethod))
                throw new Exceptions.RouteExistsException(string.Format("Operation has already been registered ({0} /{1}).  Only one route per base URI is allowed.", targetHttpMethod.ToUpper(), route.Route.Url));

            AddRoute(collection, route.Route); 
            RouteUtility.raiseOperationRouteRegistration(route, route.Route);
        }
        /// <summary>
        /// Register a REST service route against the specified routing collection.
        /// </summary>
        /// <param name="collection">Route collection object in which the service route will be added.</param>
        /// <param name="route">Service route to be registered.</param>
        public static void AddRoute(Collection<RouteBase> collection, ServiceRoute route)
        {
            if (route.RouteUri.Contains('{') || route.RouteUri.Contains('}'))
                throw new Exception("Service uri template's can not contain paramters within the URI.  Remove curly braces from the uri template.");

            if (RouteUtility.IsServiceRouteRegistered(route.Route)) 
            {
                RegisterOperations(route.ServiceType, route.Configuration, collection);
                var svc = RegisteredServices.FirstOrDefault(d => d.Key.RouteUri.TrimRoute() == route.RouteUri.TrimRoute());
                svc.Value.Add(route);
            }
            else
            {
                AddRoute(collection, route.Route);
                RegisterOperations(route.ServiceType, route.Configuration, collection);
                RouteUtility.raiseServiceRouteRegistration(route);
            }
        }

        internal static void AddRoute(Collection<RouteBase> collection, Route route)
        {
            //ensure that if a route variable (i.e. parameter token) exists that it has curley brackets at the beginning and the end of the token.
            foreach (var token in route.Url.Replace('\\', '/').Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
            {
                if ((token.Contains("{") && !token.StartsWith("{")) || (token.Contains("}") && !token.EndsWith("}")))
                    throw new MalformedRouteUriException(route.Url);
            }

            collection.Add(route);
        }

        /// <summary>
        /// Registers all associated operations.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="configuration"></param>
        /// <param name="routePrefix"></param>
        internal static void RegisterOperations(Type serviceType, Configuration configuration, Collection<RouteBase> collection)
        {
            if (serviceType == null)
                throw new Exception("Unable to register operation's on a null service type.");

            foreach (var method in serviceType.GetMethods())
            {
                var attributes = method.GetCustomAttributes(true);
                IWebInvokeAttribute webInvokeAttribute = null;

                foreach (var attr in attributes)
                {
                    if (attr is IWebInvokeAttribute)
                    {
                        webInvokeAttribute = attr as IWebInvokeAttribute;
                        break;
                    }
                }

                if (webInvokeAttribute != null)
                {
                    if (webInvokeAttribute.Method.ToUpper().Equals("GET") || webInvokeAttribute.Method.ToUpper().Equals("DELETE"))
                    {
                        foreach (var inputParameter in method.GetParameters())
                        {
                            foreach (var attr in inputParameter.GetCustomAttributes(true))
                            {
                                if (attr is HttpBodyAttribute)
                                    throw new Exception(string.Format("Unable to register service operation for service {0}.  HttpBody is not allowed on GET or DELETE operations.", serviceType.FullName));
                            }
                        }
                    }
                    OperationRoute route = new OperationRoute(serviceType, method, configuration);
                    collection.Add(route, route.HttpMethod);
                }
            }
        }

        internal static string TrimRoute(this string routeUri)
        {
            return routeUri.Replace("\\", "/").Trim(new char[] { '/', '\\', ' ' });
        }
    }
}
